package com.atguigu.filechannel.nio;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.nio.ByteBuffer;
import java.nio.channels.*;
import java.nio.charset.Charset;
import java.util.Iterator;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

/**
 * 使用SocketChannel实现TCP协议的文件传输
 * NIO中的SocketChannel和OIO中的Socket对应
 * NIO中的ServerSocketChannel和OIO中的ServerSocket对应
 */
public class Server{
    public static void main(String[] args) {
        //先启动服务端
        Server server = new Server();
        server.startServer();
    }

    private final Charset charset = Charset.forName("UTF-8");
    private final int serverPort = 9111;
    //将客户端传递的数据封装为一个对象
    static class FileData{
        String clientAddress;
        String filename; //客户端上传的文件名称
        FileChannel fileOutChannel;//输出的文件通道
    }

    //使用Map保存每个客户端传输，当OP_READ通道可读时，根据channel找到对应的对象
    Map<SelectableChannel, FileData> map = new ConcurrentHashMap<>();

    /**
     * 启动服务器
     */
    public void startServer(){
        try{
            // 1、获取Selector选择器
            Selector selector = Selector.open();

            // 2、创建一个通道，用于获取客户端的请求连接
            ServerSocketChannel serverChannel = ServerSocketChannel.open();
            ServerSocket serverSocket = serverChannel.socket();

            // 3.设置为非阻塞
            serverChannel.configureBlocking(false);

            // 4、绑定连接
            // InetSocketAddress只传入端口号，则自动绑定当前本机IP
            serverSocket.bind(new InetSocketAddress(serverPort));//即服务端开放99端口

            // 5、将该通道注册到选择器上,并注册的IO事件为：“接收新连接”
            serverChannel.register(selector, SelectionKey.OP_ACCEPT);
            System.out.println("服务端已开启，监听新连接。。。");

            // 6、遍历选择器，轮询感兴趣的I/O就绪事件（选择键集合）
            while (selector.select() > 0) {
                // 7、获取选择键集合
                Iterator<SelectionKey> it = selector.selectedKeys().iterator();
                while (it.hasNext()) {
                    // 8、获取单个的选择键，并处理
                    SelectionKey key = it.next();

                    // 9、判断key是具体的什么事件，是否为新连接事件
                    if (key.isAcceptable()) {
                        // 10、若接受的事件是“新连接”事件,就获取客户端新连接
                        ServerSocketChannel server = (ServerSocketChannel) key.channel();
                        SocketChannel socketChannel = server.accept();
                        if (socketChannel == null) continue;
                        // 11、客户端新连接，切换为非阻塞模式
                        socketChannel.configureBlocking(false);

                        // 12、将获取到的客户端socket通道再次注册到选择器，并注册为可读事件
                        // 这样下次遍历选择器时，就进入可读事件
                        socketChannel.register(selector, SelectionKey.OP_READ);

                        // 业务处理 - 每次客户端上传一个文件就创建一个对象存到map中
                        // 一个客户端对象对应一个socket通道
                        FileData fileData = new FileData();
                        fileData.clientAddress = socketChannel.getRemoteAddress().toString();
                        map.put(socketChannel, fileData);
                        System.out.println("与客户端"+fileData.clientAddress+ "连接成功...");
                    } else if (key.isReadable()) {
                        receiveFile(key);
                    }
                    // NIO的特点只会累加，已选择的键的集合不会删除
                    // 如果不删除，下一次又会被select函数选中
                    it.remove();
                }
            }

        }catch (IOException e){
            e.printStackTrace();
        }
    }

    /**
     * 接收文件
     */
    private void receiveFile(SelectionKey key){
        ByteBuffer buffer = ByteBuffer.allocate(1024);//开启内存缓冲区域
        FileData fileData = map.get(key.channel());

        SocketChannel socketChannel = (SocketChannel) key.channel();
        String directory = System.getProperty("user.dir")+"\\zaedu-charge-system\\charge-system\\target\\";//服务端收到文件的存储路径
        long start = System.currentTimeMillis();
        try {
            int len = 0;
            while ((len = socketChannel.read(buffer)) != -1) {//将客户端写入通道的数据读取并存储到buffer中
                buffer.flip();//将缓冲区翻转为读模式

                //客户端发送过来的，首先是文件名
                if (null == fileData.filename) {
                    // 文件名 decode解码为UTF-8格式，并赋值给client对象的filename属性
                    fileData.filename = (System.currentTimeMillis()+"_"+charset.decode(buffer).toString()).substring(5);

                    //先检查存储的目录是否存在
                    File dir = new File(directory);
                    if(!dir.exists()) dir.mkdir();

                    //再检查文件是否存在，不存在就创建文件，然后通过FikeChanel写入数据
                    File file = new File(directory + fileData.filename);
                    if(!file.exists()) file.createNewFile();

                    //将设定要存放的文件路径+文件名创建一个输出流通道
                    FileChannel fileChannel = new FileOutputStream(file).getChannel();
                    fileData.fileOutChannel = fileChannel;//赋值给client对象
                }
                //客户端发送过来的，最后是文件内容
                else{
                    // 通过已经创建的文件输出流通道向文件中写入数据
                    fileData.fileOutChannel.write(buffer);
                }
                buffer.clear();//清除本次缓存区内容
            }
            fileData.fileOutChannel.close();
            key.cancel();
            System.out.println("上传完毕，费时："+Long.valueOf(System.currentTimeMillis()-start)+"毫秒");
            System.out.println("文件在服务端的存储路径：" + directory + fileData.filename);
            System.out.println("");
        } catch (IOException e) {
            key.cancel();
            e.printStackTrace();
            return;
        }
    }
}